using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Practices.EnterpriseLibrary.Data;
using System.Xml;
using System.Data;
using System.Globalization;
using System.Data.Common;
using System.Xml.XPath;
using System.IO;
using Instedd.Labs.Geo.Library.GPS;
using System.Configuration;
using System.Threading;
using System.Collections.Specialized;

namespace Instedd.Labs.Forms.Library
{
    // TODO: Review(alo) the complete manager: How the survey are being created, performance issues, etc
    public class SurveyManager
    {
        Database db;

        static SurveyManager instance;

        Thread surveyLocationResolverThread;

        private string formVersion;

        IGeoCoder geoCoder;
        SurveyLocationResolver surveyLocationResolver;

        internal SurveyManager()
            : this(DatabaseFactory.CreateDatabase("FormsDatabase"))
        { }

        internal SurveyManager(Database db)
            : this(db, new GoogleGeoCoder())
        { }

        internal SurveyManager(Database db, IGeoCoder geoCoder)
            : this(db, geoCoder, ConfigurationManager.AppSettings)
        { }

        internal SurveyManager(Database db, IGeoCoder geoCoder, NameValueCollection configuration)
        {
            this.db = db;
            this.geoCoder = geoCoder;

            this.formVersion = configuration["formVersion"];
            geoCoder.Initialize(configuration["googleMapsKey"]);

            surveyLocationResolver = new SurveyLocationResolver(db, geoCoder);
        }

        public void StartLocationResolverService()
        {
            surveyLocationResolverThread = new Thread(new ThreadStart(UpdateSurveysLocations));
            surveyLocationResolverThread.Start();
        }

        public void StopLocationResolverService()
        {
            throw new Exception("The method is not implemented.");
        }

        private void UpdateSurveysLocations()
        {
            while (true)
            {
                try
                {
                    surveyLocationResolver.ResolveAllUnlocatedSurveys();
                }
                catch (Exception ex)
                {
                    // TODO: Log the error 
                }

                Thread.Sleep(15000);
            }
        }

        public static SurveyManager GetInstance()
        {
            if (instance == null)
            {
                instance = new SurveyManager();
            }

            return instance;
        }

        public Survey CreateSurvey(IDataReader reader)
        {
            Survey survey = new Survey();

            survey.Address = new Address();
            survey.Address.City = TryToCastValue<string>(reader, "city");
            survey.Address.PostalCode = TryToCastValue<string>(reader, "postalCode");
            survey.Address.State = TryToCastValue<string>(reader, "state");
            survey.Address.StreetAddress = TryToCastValue<string>(reader, "streetAddress");

            survey.AllergicDoxy = TryToCastValue<int>(reader, "allergicDoxy");
            survey.AllergicDoxy = TryToCastValue<int>(reader, "allergicDoxy");
            survey.AttendedConcert = TryToCastValue<int>(reader, "attendedConcert");
            survey.AttendeeContact = TryToCastValue<int>(reader, "attendeeContact");
            survey.CiproDistributed = TryToCastValue<int>(reader, "ciproDistributed");
            survey.Comments = TryToCastValue<string>(reader, "comments");
            survey.Deceased = TryToCastValue<int>(reader, "deceased");
            survey.DoxyDistributed = TryToCastValue<int>(reader, "doxyDistributed");
            survey.Email = TryToCastValue<string>(reader, "email");
            survey.EnteredBy = TryToCastValue<string>(reader, "enteredBy");
            survey.EnteredOn = TryToCastValue<DateTime>(reader, "enteredOn");
            survey.FirstName = TryToCastValue<string>(reader, "firstName");
            survey.ID = TryToCastValue<string>(reader, "id").Trim();
            survey.LastName = TryToCastValue<string>(reader, "lastName");
            survey.Latitude = TryToCastValue<double?>(reader, "latitude");
            survey.Longitude = TryToCastValue<double?>(reader, "longitude");
            survey.Phone = TryToCastValue<string>(reader, "phone1");
            survey.Pregnant = TryToCastValue<int>(reader, "pregnant");
            survey.ReceivedIvTreatment = TryToCastValue<int>(reader, "receivedIvTreatment");
            survey.Recovered = TryToCastValue<int>(reader, "recovered");
            survey.SymptomsPresent = TryToCastValue<int>(reader, "symptomsPresent");
            survey.TimeOfOnset = TryToCastValue<DateTime>(reader, "timeOfOnset");
            survey.Under9years = TryToCastValue<int>(reader, "under9years");
            survey.FollowUpComments = TryToCastValue<string>(reader, "followUpComments");
            survey.FollowUpEnteredOn = TryToCastValue<DateTime>(reader, "followUpEnteredOn");
            survey.DoxyCiproAllergy = TryToCastValue<int>(reader, "doxyciproAllergy");
            survey.TotalOccupants = TryToCastValue<int>(reader, "totalOccupants");

            return survey;
        }

        private T TryToCastValue<T>(IDataReader survey, string fieldName)
        {
            object value = survey[fieldName];

            if (value != DBNull.Value)
            {
                return (T)value;
            }

            return default(T);
        }


        public XPathDocument CreateSurvey(string xmlSurveyContent)
        {
            return new XPathDocument(new StringReader(xmlSurveyContent));
        }

        private string GetSurveyId(XPathNavigator survey)
        {
            return survey.SelectSingleNode("id").Value;
        }

        public int Update(XPathNavigator survey, bool resetLocationResolutionAttemps)
        {
            ThrowsIfFormVersionIsInvalid(survey);

            using (DbCommand command = db.GetSqlStringCommand(GenerateUpdateSurveySQL()))
            {
                foreach (XPathNavigator surveyElement in survey.SelectChildren(XPathNodeType.Element))
                {
                    SurveyFieldDescriptor descriptor = SurveyFieldDescriptorProvider.SurveyDescriptors.Find(
                        delegate(SurveyFieldDescriptor target)
                        {
                            return surveyElement.Name == target.XmlElementName;
                        }
                    );

                    if (descriptor != null)
                    {
                        db.AddInParameter(
                            command,
                            descriptor.DbFieldName,
                            descriptor.Type,
                            ParseValue(descriptor, surveyElement.Value));
                    }
                }

                if (resetLocationResolutionAttemps)
                {
                    this.surveyLocationResolver.ResetLocationResolutionAttemps(GetSurveyId(survey));
                }

                return db.ExecuteNonQuery(command);
            }
        }

        public void Save(XPathNavigator survey)
        {
            ThrowsIfFormVersionIsInvalid(survey);

            using (DbCommand command = db.GetSqlStringCommand(GenerateInsertSurveySQL()))
            {
                foreach (XPathNavigator surveyElement in survey.SelectChildren(XPathNodeType.Element))
                {
                    SurveyFieldDescriptor descriptor = SurveyFieldDescriptorProvider.SurveyDescriptors.Find(
                        delegate(SurveyFieldDescriptor target)
                        {
                            return surveyElement.Name == target.XmlElementName;
                        }
                    );

                    if (descriptor != null)
                    {
                        db.AddInParameter(
                            command,
                            descriptor.DbFieldName,
                            descriptor.Type,
                            ParseValue(descriptor, surveyElement.Value));
                    }

                    //TODO: If the descriptor is not defined (it's null) we should log it.
                }

                db.ExecuteNonQuery(command);
            }
        }

        private object ParseValue(SurveyFieldDescriptor descriptor, string value)
        {
            if (value == null)
            {
                return DBNull.Value;
            }
            else if (descriptor.Type == DbType.String)
            {
                return value;
            }
            else if (value == string.Empty)
            {
                return DBNull.Value;
            }
            else if (descriptor.Type == DbType.Int32)
            {
                return int.Parse(value, CultureInfo.InvariantCulture);
            }
            else if (descriptor.Type == DbType.DateTime)
            {
                return DateTime.Parse(value, CultureInfo.InvariantCulture);
            }
            else if (descriptor.Type == DbType.Decimal)
            {
                return decimal.Parse(value, CultureInfo.InvariantCulture);
            }

            return DBNull.Value;
        }

        private string GenerateInsertSurveySQL()
        {
            StringBuilder fields = new StringBuilder();
            SurveyFieldDescriptorProvider.SurveyDescriptors.ForEach(
                delegate(SurveyFieldDescriptor descriptor)
                {
                    if (fields.Length != 0)
                    {
                        fields.Append(",");
                    }
                    fields.Append(string.Format("[{0}]", descriptor.DbFieldName));
                }
            );

            StringBuilder parameters = new StringBuilder();
            SurveyFieldDescriptorProvider.SurveyDescriptors.ForEach(
                delegate(SurveyFieldDescriptor descriptor)
                {
                    if (parameters.Length != 0)
                    {
                        parameters.Append(",");
                    }
                    parameters.Append(string.Format("@{0}", descriptor.DbFieldName));
                }
            );

            return string.Format("INSERT INTO [Forms] ({0}) VALUES ({1})", fields.ToString(), parameters.ToString());
        }

        private string GenerateUpdateSurveySQL()
        {
            StringBuilder fields = new StringBuilder();
            SurveyFieldDescriptorProvider.SurveyDescriptors.ForEach(
                delegate(SurveyFieldDescriptor descriptor)
                {
                    if (descriptor.DbFieldName != "id")
                    {
                        if (fields.Length != 0)
                        {
                            fields.Append(",");
                        }
                        fields.Append(string.Format("[{0}]=@{1}", descriptor.DbFieldName, descriptor.DbFieldName));
                    }
                }
            );

            return string.Format("UPDATE [Forms] SET {0} WHERE [id]=@id", fields.ToString());
        }

        private const string SelectSurveyByIdSQL = @"SELECT * FROM [Forms] WHERE [id] = @id";

        public XmlDocument GetSurveyById(string id)
        {
            using (IDataReader reader = GetSurveyByIdAsDataReader(id))
            {
                if (reader.Read())
                {
                    return CreateSurveyAsXmlDocument(reader);
                }
            }

            return null;
        }

        public IDataReader GetSurveyByIdAsDataReader(string id)
        {
            using (DbCommand command = db.GetSqlStringCommand(SelectSurveyByIdSQL))
            {
                db.AddInParameter(command, "id", DbType.String, id);
                return db.ExecuteReader(command);
            }
        }

        private XmlDocument CreateSurveyAsXmlDocument(IDataReader reader)
        {
            XmlDocument document = new XmlDocument();
            XmlElement survey = document.CreateElement("Survey");
            document.AppendChild(survey);

            foreach (SurveyFieldDescriptor descriptor in SurveyFieldDescriptorProvider.SurveyDescriptors)
            {
                XmlElement element = document.CreateElement(descriptor.XmlElementName);
                element.InnerText = GetStringValue(reader, descriptor);

                survey.AppendChild(element);
            }

            return document;
        }

        private string GetStringValue(IDataReader reader, SurveyFieldDescriptor descriptor)
        {
            object value = reader[descriptor.DbFieldName];

            if (value == DBNull.Value)
            {
                return null;
            }

            if (descriptor.Type == DbType.DateTime)
            {
                return ((DateTime)value).ToString("MM-dd-yyyy HH:mm", CultureInfo.CurrentUICulture);
            }
            else
            {
                return value.ToString();
            }
        }

        private const string SelectAllSurveysSQL = @"SELECT * FROM [Forms]";

        public DataSet GetSurveysAsDataSet()
        {
            using (DbCommand command = db.GetSqlStringCommand(SelectAllSurveysSQL))
            {
                return db.ExecuteDataSet(command);
            }
        }

        public IDataReader GetSurveysAsDataReader()
        {
            using (DbCommand command = db.GetSqlStringCommand(SelectAllSurveysSQL))
            {
                return db.ExecuteReader(command);
            }
        }

        private const string DeleteSurveyByIdSQL = @"DELETE FROM [Forms] WHERE [id] = @surveyId";

        public void DeleteSurvey(string surveyId)
        {
            using (DbCommand command = db.GetSqlStringCommand(DeleteSurveyByIdSQL))
            {
                db.AddInParameter(command, "surveyId", DbType.String, surveyId);
                db.ExecuteNonQuery(command);
            }
        }

        private const string DeleteAllSurveysSQL = @"DELETE FROM [Forms]";
        public void DeleteAllSurveys()
        {
            using (DbCommand command = db.GetSqlStringCommand(DeleteAllSurveysSQL))
            {
                db.ExecuteNonQuery(command);
            }
        }

        private void ThrowsIfFormVersionIsInvalid(XPathNavigator survey)
        {
            if (!string.IsNullOrEmpty(formVersion))
            {
                string surveyVersion = survey.GetAttribute("version", string.Empty);

                if (formVersion != surveyVersion)
                {
                    // TODO: Move to resources.
                    throw new InvalidFormVersionException(string.Format("The form version '{0}' is not supported anymore. Please update your client to version '{1}' and try again.", surveyVersion, formVersion));
                }
            }
        }

        public string FormVersion { get { return this.formVersion; } }

        public void WriteAllSurveysUsingCsvFormat(StreamWriter writer, bool includeHeader)
        {
            WriteAllSurveysUsingDelimiterFormat(writer, includeHeader, ",");
        }

        private void WriteAllSurveysUsingDelimiterFormat(StreamWriter writer, bool includeHeader, string delimiter)
        {
            if (includeHeader)
            {
                WriteLineUsingDelimiterFormat(writer, delegate(SurveyFieldDescriptor descriptor) { writer.Write(descriptor.DbFieldName); }, delimiter);
            }

            using (IDataReader reader = GetSurveysAsDataReader())
            {
                while (reader.Read())
                {
                    WriteLineUsingDelimiterFormat(writer, delegate(SurveyFieldDescriptor descriptor) { writer.Write(GetStringValue(reader, descriptor)); }, delimiter);
                }
            }
        }

        private void WriteLineUsingDelimiterFormat(StreamWriter writer, Action<SurveyFieldDescriptor> lineWriter, string delimiter)
        {
            bool isFirstValue = true;
            SurveyFieldDescriptorProvider.SurveyDescriptors.ForEach(
                delegate(SurveyFieldDescriptor descriptor)
                {
                    if (!isFirstValue)
                    {
                        writer.Write(delimiter);
                    }
                    lineWriter(descriptor);
                    isFirstValue = false;
                }
            );

            writer.Write(System.Environment.NewLine);
        }

        public List<Survey> GetAllSurveys()
        {
            List<Survey> surveyList = new List<Survey>();
            using (IDataReader reader = GetSurveysAsDataReader())
            {
                while (reader.Read())
                {
                    Survey survey = CreateSurvey(reader);
                    surveyList.Add(survey);
                }
            }

            return surveyList;
        }

        public AggregatedSurveyTotals CalculateSurveyTotals()
        {
            AggregatedSurveyTotals totals = new AggregatedSurveyTotals();

            using (IDataReader reader = GetSurveysAsDataReader())
            {
                while (reader.Read())
                {
                    Survey survey = CreateSurvey(reader);

                    totals.TotalDeceased += survey.TotalDeceased;
                    totals.TotalExposed += survey.TotalExposed;
                    totals.TotalExposedAndIll += survey.TotalExposedAndIll;
                    totals.TotalExposedNotIll += survey.TotalExposedNotIll;
                    totals.TotalReceivedTreatment += survey.TotalReceivedTreatment;
                    totals.TotalRecovered += survey.TotalRecovered;
                    totals.TotalWell += survey.TotalWell;
                    totals.TotalAllergicOccupantsToCiproOrDoxy += survey.AllergicDoxy;
                    totals.TotalCiproDistributed += survey.CiproDistributed;
                    totals.TotalDoxyDistributed += survey.DoxyDistributed;
                }
            }

            return totals;
        }
    }
}